/*
 * Copyright (c) 2007, TopCoder, Inc. All rights reserved.
 */
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using TopCoder.Util.Calendar.Entry;

namespace TopCoder.WinForms.Controls.CalendarView.Views
{
    /// <summary>
    /// <para>This class draws the calendar data for a week to the drawing panel. This class handles one week at
    /// a time, so the next and previous buttons move forward and back one week at a time. The start of the week
    /// and the end of the week are modifiable through the StartOfWeek and EndOfWeek properties in this class.
    /// </para>
    /// <remarks>This class itself is mutable, as it inherits the mutability of its parent class.</remarks>
    /// </summary>
    /// <author>Ghostar</author>
    /// <author>yext</author>
    /// <author>argolite</author>
    /// <author>alex.dolin</author>
    /// <version>1.1</version>
    /// <copyright>Copyright (c) 2007, TopCoder, Inc. All rights reserved.</copyright>
    public class WeeklyCalendarView : AbstractCalendarView
    {
        /// <summary>
        /// The minimum number of rows.
        /// </summary>
        private const int MIN_ROW_COUNT = 4;

        /// <summary>
        /// The minimum width of columns.
        /// </summary>
        private const int MIN_COLUMN_WIDTH = 0;

        /// <summary>
        /// The minimum height of rows.
        /// </summary>
        private const int MIN_ROW_HEIGHT = 20;

        /// <summary>
        /// The left margin for text.
        /// </summary>
        internal const int PADDING_LEFT = 10;

        /// <summary>
        /// The right margin for text.
        /// </summary>
        internal const int PADDING_RIGHT = 10;

        /// <summary>
        /// The top margin for text.
        /// </summary>
        internal const int PADDING_TOP = 5;

        /// <summary>
        /// The bottom margin for text.
        /// </summary>
        internal const int PADDING_BOTTOM = 5;

        /// <summary>
        /// The format string for the first row, e.g. "Sunday, Dec 03".
        /// </summary>
        private const string LONG_DATE_FORMAT_STRING = "dddd";

        /// <summary>
        /// The format string for the second row, e.g. "3".
        /// </summary>
        private const string SHORT_DATE_FORMAT_STRING = "%d";

        /// <summary>
        /// The font settings for the header text on the 1st row (e.g. "Sunday, Dec 03").
        /// </summary>
        private readonly static Font dayHeaderLongTextFont = new Font("Arial", 10F, FontStyle.Bold);

        /// <summary>
        /// The font settings for the header text on the 2nd row (e.g. "3").
        /// </summary>
        private readonly static Font dayHeaderShortTextFont = new Font("Arial", 10F);

        /// <summary>
        /// The font settings for the item text.
        /// </summary>
        internal readonly static Font itemFont = new Font("Arial", 10F);

        /// <summary>
        /// The brush settings for the header text on the 1st row.
        /// </summary>
        private readonly static Brush dayHeaderLongTextBrush = new SolidBrush(Color.Gray);

        /// <summary>
        /// The brush settings for the header text on the 2nd row.
        /// </summary>
        private readonly static Brush dayHeaderShortTextBrush = new SolidBrush(Color.Black);

        /// <summary>
        /// The brush settings for the background color of the header on the 1st row.
        /// </summary>
        private readonly static Brush dayHeaderBackBrush = new SolidBrush(Color.LightGray);

        /// <summary>
        /// The brush settings for the background color of the items.
        /// </summary>
        private readonly static Brush itemBackBrush = new SolidBrush(Color.WhiteSmoke);

        /// <summary>
        /// The brush settings for the background color for today.
        /// </summary>
        private readonly static Brush todayBackBrush = new SolidBrush(Color.LemonChiffon);

        /// <summary>
        /// The brush settings for the text of the items.
        /// </summary>
        internal readonly static Brush itemBrush = new SolidBrush(Color.Black);

        /// <summary>
        /// The pen setting for the border of the cells.
        /// </summary>
        private readonly static Pen borderPen = new Pen(Color.LightGray);

        /// <summary>
        /// This member holds the day of the week that is used as the start day for weeks shown in this view. This
        /// value defaults to Sunday and is modifiable through the StartOfWeek property.
        /// </summary>
        private DayOfWeek startOfWeek = DayOfWeek.Sunday;

        /// <summary>
        /// This member holds the day of the week that is used as the end day for weeks shown in this view. This value
        /// defaults to Saturday and is modifiable through the EndOfWeek  property.
        /// </summary>
        private DayOfWeek endOfWeek = DayOfWeek.Saturday;

        /// <summary>
        /// <para>Represents the start date of the current view.</para>
        /// </summary>
        private DateTime startDate;

        /// <summary>
        /// Entries sorted by day of week.
        /// </summary>
        private IList<CalendarEntry>[] sortedEntries;

        /// <summary>
        /// Represents the name of the day view, to be used by the navigation panels.
        /// May be null, which would result in navigation not transfering to new view. Will not be empty.
        /// Managed by DayViewType property.
        /// </summary>
        private string dayViewType;

        /// <summary>
        /// Measured row height.
        /// </summary>
        private float measuredRowHeight;

        /// <summary>
        /// Measured column width.
        /// </summary>
        private float measuredColumnWidth;

        /// <summary>
        /// Caption for the days of week.
        /// </summary>
        private string[] dayHeaderLongText;

        /// <summary>
        /// Caption for the days of week.
        /// </summary>
        private string[] dayHeaderShortText;

        /// <summary>
        /// Total number of items in each column.
        /// </summary>
        private int itemRowCount;

        /// <summary>
        /// Indicates whether the data has changed since last view update.
        /// </summary>
        private bool dataChanged = false;

        /// <summary>
        /// <para>Use the specified date to filter the calendar entries.</para>
        /// </summary>
        private void FilterData()
        {
            startDate = DateOfInterest.Date;
            // Adjust the start date to the first day of the defined week.
            while (startDate.DayOfWeek != startOfWeek)
            {
                startDate = startDate.AddDays(-1F);
            }
            DateTime endDate = startDate;

            // The number of the days displayed in a week.
            int days = 1;

            // Calculate the endDate.
            while (endDate.DayOfWeek != endOfWeek)
            {
                endDate = endDate.AddDays(1F);
                days++;
            }

            // Set the title text.
            TitleLabelText = startDate.ToLongDateString() + " - " + endDate.ToLongDateString();

            // Select the entries in the week.
            CurrentData = Helper.SelectEntriesByDateRange(AllData, startDate, endDate);

            // Set up the sortedEntries array.
            sortedEntries = new IList<CalendarEntry>[days];
            for (int i = 0; i < days; i++)
            {
                sortedEntries[i] = new List<CalendarEntry>();
            }

            // Fill the array.
            foreach (CalendarEntry entry in CurrentData)
            {
                int offset = (entry.Date.Date - startDate).Days;
                sortedEntries[offset].Add(entry);
            }

            // Recreate all panels because current data has changed
            dataChanged = true;
            DrawingPanel.Invalidate();
        }

        /// <summary>
        /// Creates child panels.
        /// </summary>
        private void CreatePanels()
        {
            DrawingPanel.SuspendLayout();
            Helper.ReleaseChildControls(DrawingPanel);
            int dayCount = sortedEntries.Length;
            DateTime viewDate = startDate;
            for (int day = 0; day < dayCount; day++)
            {
                // Create a new navigation panel for day
                NavigationPanel dayPanel = new NavigationPanel(this, DayViewType, viewDate);
                dayPanel.Parent = DrawingPanel;
                dayPanel.Paint += new DayPaintHandler(this, day).OnPaint;

                Brush currentBackBrush = itemBackBrush;
                if (viewDate.Equals(DateTime.Now.Date))
                {
                    currentBackBrush = todayBackBrush;
                }

                foreach (CalendarEntry entry in sortedEntries[day])
                {
                    // Create a new popup panel for entry
                    DescriptionPopupPanel entryPanel =
                        new DescriptionPopupPanel(entry, false, this, DayViewType, viewDate);
                    entryPanel.Parent = dayPanel;
                    entryPanel.Paint += new EntryPaintHandler(this, entry, currentBackBrush).OnPaint;
                }
                viewDate = viewDate.AddDays(1);
            }
            DrawingPanel.ResumeLayout();
        }

        /// <summary>
        /// Updates child panels layout.
        /// </summary>
        private void UpdatePanelsLayout()
        {
            if(dataChanged)
            {
                CreatePanels();
            }
            int dayCount = sortedEntries.Length;
            itemRowCount = MIN_ROW_COUNT;

            measuredColumnWidth = MIN_COLUMN_WIDTH;
            measuredRowHeight = MIN_ROW_HEIGHT;
            IDictionary<CalendarEntry, float> entryWidth = new Dictionary<CalendarEntry, float>(CurrentData.Count);

            DateTime[] dates = new DateTime[dayCount];
            dayHeaderLongText = new string[dayCount];
            dayHeaderShortText = new string[dayCount];

            #region Measure the size of the panel

            using (Graphics g = DrawingPanel.CreateGraphics())
            {
                dates[0] = startDate;
                for (int i = 0; i < sortedEntries.Length; i++)
                {
                    if (i > 0)
                    {
                        dates[i] = dates[i - 1].AddDays(1F);
                    }

                    // The header text.
                    dayHeaderLongText[i] = dates[i].ToString(LONG_DATE_FORMAT_STRING);

                    // Measure the size of the header text.
                    SizeF size = g.MeasureString(dayHeaderLongText[i], dayHeaderLongTextFont);
                    measuredColumnWidth = Math.Max(measuredColumnWidth, size.Width + PADDING_LEFT + PADDING_RIGHT);
                    measuredRowHeight = Math.Max(measuredRowHeight, size.Height + PADDING_TOP + PADDING_BOTTOM);

                    dayHeaderShortText[i] = dates[i].ToString(SHORT_DATE_FORMAT_STRING);

                    // Measure the size of the header text on the 2nd row.
                    size = g.MeasureString(dayHeaderShortText[i], dayHeaderShortTextFont);
                    measuredColumnWidth = Math.Max(measuredColumnWidth, size.Width + PADDING_LEFT + PADDING_RIGHT);
                    measuredRowHeight = Math.Max(measuredRowHeight, size.Height + PADDING_TOP + PADDING_BOTTOM);

                    itemRowCount = Math.Max(itemRowCount, sortedEntries[i].Count);

                    foreach (CalendarEntry entry in sortedEntries[i])
                    {
                        // Measure the size of the calendar entry item.
                        size = g.MeasureString(entry.Type.Description + ": " + entry.Description, itemFont);
                        measuredColumnWidth = Math.Max(
                            measuredColumnWidth, size.Width + PADDING_LEFT + PADDING_RIGHT);
                        measuredRowHeight = Math.Max(
                            measuredRowHeight, size.Height + PADDING_TOP + PADDING_BOTTOM);

                        entryWidth[entry] = size.Width + PADDING_LEFT + PADDING_RIGHT;
                    }
                }
            }
            float measuredTotalWidth = measuredColumnWidth * dayCount;
            float measuredTotalHeight = measuredRowHeight * (itemRowCount + 2);

            // Adjust the size of DrawingPanel.
            Helper.AdjustPanelSize(HolderPanel, DrawingPanel,
                (int)Math.Ceiling(measuredTotalWidth), (int)Math.Ceiling(measuredTotalHeight), ColumnWidthRendering);

            measuredTotalWidth = DrawingPanel.Width;
            measuredColumnWidth = measuredTotalWidth / dayCount;

            #endregion

            #region Draw the panel:

            // The position of the day panel.
            float x = 0;
            for (int day = 0; day < dayCount; day++)
            {
                // Create a new navigation panel for day
                Panel dayPanel =(Panel)DrawingPanel.Controls[day];
                Helper.UpdatePanel(dayPanel, (int)Math.Ceiling(x), 0,
                    (int)Math.Floor(measuredColumnWidth), (int)Math.Floor(measuredTotalHeight));

                float y = measuredRowHeight*2;
                for (int row = 0; row < sortedEntries[day].Count; row++)
                {
                    CalendarEntry entry = sortedEntries[day][row];

                    // Create a new popup panel for entry
                    CalendarEntryPopupPanel entryPanel = (CalendarEntryPopupPanel)dayPanel.Controls[row];
                    entryPanel.Truncated = entryWidth[entry] > measuredColumnWidth;
                    Helper.UpdatePanel(entryPanel, 0, (int)Math.Ceiling(y),
                        (int)Math.Floor(measuredColumnWidth), (int)Math.Floor(measuredRowHeight));

                    y += measuredRowHeight;
                }

                x += measuredColumnWidth;
            }

            #endregion

            dataChanged = false;
        }

        /// <summary>
        /// Represents the date around which this view should be built. The resulting view must inlcude this date.
        /// By default, the current date is used.
        /// </summary>
        /// <value>The date around which this view should be built.</value>
        public override DateTime DateOfInterest
        {
            set
            {
                base.DateOfInterest = value;
                FilterData();
            }
        }

        /// <summary>
        /// <para>This method draws the drawing panel with the current calendar entries.</para>
        /// </summary>
        /// <param name="sender">The sender of the event.</param>
        /// <param name="e">The event, containing the graphic.</param>
        protected override void OnDrawingPanelPaint(object sender, PaintEventArgs e)
        {
            UpdatePanelsLayout();
        }

        /// <summary>
        /// This property allows get / set access to the startOfWeek member variable, used to represent the day of the
        /// week that is the start of a week shown in this view.
        /// </summary>
        /// <exception cref="ArgumentException">
        /// ArgumentException if the value given isn't a valid enum value
        /// </exception>
        public DayOfWeek StartOfWeek
        {
            get
            {
                return startOfWeek;
            }
            set
            {
                if (!Enum.IsDefined(typeof(DayOfWeek), value))
                {
                    throw new ArgumentException("StartOfWeek should be a valid enum.", "StartOfWeek");
                }

                startOfWeek = value;

                startDate = DateTime.Now.Date;
                FilterData();
            }
        }

        /// <summary>
        /// This property allows get / set access to the endOfWeek member variable, used to represent the day of the
        /// week that is the end of a week shown in this view.
        /// </summary>
        /// <exception cref="ArgumentException">
        /// ArgumentException if the value given isn't a valid enum value
        /// </exception>
        public DayOfWeek EndOfWeek
        {
            get
            {
                return endOfWeek;
            }
            set
            {
                if (!Enum.IsDefined(typeof(DayOfWeek), value))
                {
                    throw new ArgumentException("EndOfWeek should be a valid enum.", "EndOfWeek");
                }

                endOfWeek = value;
                startDate = DateTime.Now.Date;
                FilterData();
            }
        }

        /// <summary>
        /// Represents the name of the day view, to be used by the navigation panels.
        /// May be null, which would result in navigation not transfering to new view.
        /// Empty is set to null.
        /// </summary>
        /// <value>The name of the day view, to be used by the navigation panels.</value>
        public string DayViewType
        {
            get
            {
                return dayViewType;
            }
            set
            {
                dayViewType = Helper.GetNullIfEmpty(value);
            }
        }

        /// <summary>
        /// Creates a new instance of this class.
        /// </summary>
        /// <param name="parent">The parent calendar view control.</param>
        /// <exception cref="ArgumentNullException">If <paramref name="parent"/> is null.</exception>
        public WeeklyCalendarView(CalendarViewControl parent)
            : base(parent)
        {
            FilterData();
        }

        /// <summary>
        /// <para>This method is called when the "Next" button is pressed on the header of the view.
        /// This method should get all calendar data for the week that is after the current week being shown.
        /// </para>
        /// </summary>
        /// <param name="sender">The next button that was pressed.</param>
        /// <param name="e">The event args.</param>
        protected override void OnNextButtonClick(object sender, EventArgs e)
        {
            DateOfInterest += new TimeSpan(7, 0, 0, 0);
        }

        /// <summary>
        /// <para>This method is called when the "Previous" button is pressed on the header of the view.
        /// This method should get all calendar data for the week that is before the current week being shown.
        /// </para>
        /// </summary>
        /// <param name="sender">The previous button that was clicked.</param>
        /// <param name="e">The event args.</param>
        protected override void OnPreviousButtonClick(object sender, EventArgs e)
        {
            DateOfInterest -= new TimeSpan(7, 0, 0, 0);
        }

        /// <summary>
        /// Private helper class to draw day panel.
        /// </summary>
        /// <remarks>Thread Safety: This class is immutable and thread-safe.</remarks>
        private class DayPaintHandler
        {
            /// <summary>
            /// The parent weekly calendar view of this panel.
            /// </summary>
            private readonly WeeklyCalendarView view;

            /// <summary>
            /// The day of the week for this panel.
            /// </summary>
            private readonly int day;

            /// <summary>
            /// Creates a new instance of the class.
            /// </summary>
            /// <param name="view">The parent weekly calendar view of this panel.</param>
            /// <param name="day">The day of the week for this panel.</param>
            public DayPaintHandler(WeeklyCalendarView view, int day)
            {
                this.view = view;
                this.day = day;
            }

            /// <summary>
            /// Processes <see cref="Control.Paint"/> event for one day panel.
            /// </summary>
            /// <param name="sender">The sender of the event.</param>
            /// <param name="e">The event, containing the graphic.</param>
            public void OnPaint(object sender, PaintEventArgs e)
            {
                Panel dayPanel = (Panel)sender;
                Graphics g = e.Graphics;

                float y = 0;
                // The first header cell
                // The background color:
                g.FillRectangle(dayHeaderBackBrush, 0, y, dayPanel.ClientSize.Width, view.measuredRowHeight);
                // The text:
                g.DrawString(view.dayHeaderLongText[day], dayHeaderLongTextFont, dayHeaderLongTextBrush,
                             PADDING_LEFT, y + PADDING_TOP);
                y += view.measuredRowHeight;

                // The second header cell
                Brush currentBackBrush = itemBackBrush;
                DateTime date = view.startDate;
                date = date.AddDays(day);
                if (date.Equals(DateTime.Now.Date))
                {
                    currentBackBrush = todayBackBrush;
                }
                // The background color:
                g.FillRectangle(currentBackBrush, 0, y, dayPanel.ClientSize.Width, view.measuredRowHeight);
                // The border:
                g.DrawRectangle(borderPen, 0, y, view.measuredColumnWidth, view.measuredRowHeight);
                // The text:
                g.DrawString(view.dayHeaderShortText[day], dayHeaderShortTextFont, dayHeaderShortTextBrush,
                             PADDING_LEFT, y + PADDING_TOP);
                y += view.measuredRowHeight * (view.sortedEntries[day].Count + 1);

                for (int row = view.sortedEntries[day].Count; row < view.itemRowCount; row++)
                {
                    // The default background color:
                    g.FillRectangle(currentBackBrush, 0, y, dayPanel.ClientSize.Width, view.measuredRowHeight);
                    g.DrawRectangle(borderPen, 0, y, dayPanel.ClientSize.Width, view.measuredRowHeight);
                    y += view.measuredRowHeight;
                }
            }
        }

        /// <summary>
        /// Private helper class to draw one entry.
        /// </summary>
        /// <remarks>Thread Safety: This class is immutable and thread-safe.</remarks>
        private class EntryPaintHandler
        {
            /// <summary>
            /// The parent weekly calendar view of this panel.
            /// </summary>
            private readonly WeeklyCalendarView view;

            /// <summary>
            /// The calendar entry instance which holds data to display.
            /// </summary>
            private readonly CalendarEntry entry;

            /// <summary>
            /// The brush to use if the entry has transparent color.
            /// </summary>
            private readonly Brush currentBackBrush;

            /// <summary>
            /// Creates a new instance of the class.
            /// </summary>
            /// <param name="view">The parent weekly calendar view of this panel.</param>
            /// <param name="entry">The calendar entry instance which holds data to display.</param>
            /// <param name="currentBackBrush">The brush to use if the entry has transparent color.</param>
            public EntryPaintHandler(WeeklyCalendarView view, CalendarEntry entry, Brush currentBackBrush)
            {
                this.view = view;
                this.entry = entry;
                this.currentBackBrush = currentBackBrush;
            }

            /// <summary>
            /// Processes <see cref="Control.Paint"/> event for one entry panel.
            /// </summary>
            /// <param name="sender">The sender of the event.</param>
            /// <param name="e">The event, containing the graphic.</param>
            public void OnPaint(object sender, PaintEventArgs e)
            {
                Panel entryPanel = (Panel)sender;
                Graphics g = e.Graphics;

                // The defined background color for the type:
                Color definedColorForType = view.GetColor(entry.Type);
                Brush entryBrush = (definedColorForType == Color.Transparent) ?
                                   currentBackBrush : new SolidBrush(definedColorForType);

                g.FillRectangle(entryBrush, 0, 0,
                                 entryPanel.ClientSize.Width - 1,
                                 entryPanel.ClientSize.Height - 1);
                // The text:
                RectangleF cellRect = new RectangleF(PADDING_LEFT, PADDING_TOP,
                         view.measuredColumnWidth - PADDING_RIGHT - PADDING_LEFT,
                         view.measuredRowHeight - PADDING_BOTTOM - PADDING_TOP);

                g.DrawString(entry.FullName, itemFont, itemBrush,
                              cellRect, Helper.StringTrimFormat);
            }
        }

    }
}
